package com.fanxuankai.boot.log.interceptor;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.net.NetUtil;
import com.alibaba.fastjson.JSONObject;
import com.fanxuankai.boot.log.*;
import com.fanxuankai.boot.log.annotation.Log;
import com.fanxuankai.boot.log.autoconfigure.LogProperties;
import com.fanxuankai.boot.log.store.LogStore;
import com.google.common.reflect.TypeToken;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.lang.NonNull;

import java.util.Date;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * 方法增强具体实现
 *
 * @author fanxuankai
 */
public class LogMethodInterceptor implements MethodInterceptor, ApplicationContextAware {
    private static final Logger LOGGER = LoggerFactory.getLogger(LogMethodInterceptor.class);
    private final LogStore logStore;
    private final LogDetailService logDetailService;
    private final ClientInfoService clientInfoService;
    private final LogProperties properties;
    private final ThreadPoolExecutor threadPoolExecutor;
    private Map<? extends Class<?>, JsonFilter> jsonFilterCache;

    private static final Set<String> DEFAULT_EXCLUDE_CLASSES_TO_JSON = CollectionUtil.newHashSet("javax.servlet.ServletRequest",
            "javax.servlet.ServletResponse", "org.springframework.core.io.InputStreamSource");

    public LogMethodInterceptor(LogStore logStore, LogDetailService logDetailService,
                                ClientInfoService clientInfoService, LogProperties properties,
                                ThreadPoolExecutor threadPoolExecutor) {
        this.logStore = logStore;
        this.logDetailService = logDetailService;
        this.clientInfoService = clientInfoService;
        this.properties = properties;
        this.threadPoolExecutor = threadPoolExecutor;
    }

    @Override
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        return enhanceProceed(methodInvocation);
    }

    /**
     * 增强处理
     *
     * @param methodInvocation /
     * @return /
     * @throws Throwable /
     */
    private Object enhanceProceed(MethodInvocation methodInvocation) throws Throwable {
        // 表达式形式注解为空
        Log logAnnotation = AnnotationUtils.findAnnotation(methodInvocation.getMethod(), Log.class);
        if (logAnnotation == null) {
            logAnnotation = AnnotationUtils.findAnnotation(methodInvocation.getThis().getClass(), Log.class);
        }
        LogInfo logInfo = new LogInfo();
        logInfo.setOperationException(Boolean.FALSE);
        Object proceed = null;
        long start = 0;
        long end = 0;
        try {
            start = System.currentTimeMillis();
            proceed = methodInvocation.proceed();
            end = System.currentTimeMillis();
        } catch (Throwable throwable) {
            end = System.currentTimeMillis();
            logInfo.setOperationException(Boolean.TRUE);
            logInfo.setExceptionDetail(ExceptionUtil.stacktraceToString(throwable));
            throw throwable;
        } finally {
            try {
                logInfo.setTotalTimeMillis(end - start);
                setupLogInfo(logInfo, logAnnotation, methodInvocation);
                setupReturnValue(logAnnotation, logInfo, proceed);
                if (properties.isAsync() && threadPoolExecutor != null) {
                    threadPoolExecutor.execute(() -> logStore.store(logInfo));
                } else {
                    logStore.store(logInfo);
                }
            } catch (Throwable throwable) {
                LOGGER.error("记录日志异常", throwable);
            }
        }
        return proceed;
    }

    /**
     * LogInfo 对象填充信息
     *
     * @param logInfo          /
     * @param log              /
     * @param methodInvocation /
     */
    private void setupLogInfo(LogInfo logInfo, Log log, MethodInvocation methodInvocation) {
        logInfo.setClassName(methodInvocation.getThis().getClass().getName());
        logInfo.setMethodName(methodInvocation.getMethod().getName());
        logInfo.setServerIp(NetUtil.getLocalhostStr());
        logInfo.setCreateTime(new Date());
        logInfo.setApplication(properties.getApplication());
        boolean logUser;
        if (log != null) {
            logInfo.setResource(log.resource());
            logInfo.setResourceId(log.resourceId());
            logInfo.setSafetyLevel(log.safetyLevel().ordinal());
            logUser = log.user();
        } else {
            logInfo.setSafetyLevel(properties.getGlobalConfig().getSafetyLevel().ordinal());
            logUser = properties.getGlobalConfig().isUser();
        }
        setupParams(log, logInfo, methodInvocation);
        if (logUser) {
            User user = logDetailService.getUser();
            if (user != null) {
                logInfo.setUserId(user.getId());
                logInfo.setUsername(user.getName());
            }
        }
        setupClientInfo(logInfo);
    }

    /**
     * 客户端信息赋值
     *
     * @param logInfo /
     */
    private void setupClientInfo(LogInfo logInfo) {
        if (clientInfoService == null) {
            return;
        }
        logInfo.setUrl(clientInfoService.getUrl());
        logInfo.setClientIp(clientInfoService.getIp());
        logInfo.setClientAddress(logDetailService.getAddress(logInfo.getClientIp()));
        logInfo.setBrowser(clientInfoService.getBrowser());
    }

    /**
     * 参数赋值
     *
     * @param log              /
     * @param logInfo          /
     * @param methodInvocation /
     */
    private void setupParams(Log log, LogInfo logInfo, MethodInvocation methodInvocation) {
        Class<? extends JsonFilter>[] paramsFilters = null;
        if (log == null) {
            if (!properties.getGlobalConfig().isParams()) {
                return;
            }
        } else {
            paramsFilters = log.paramsFilters();
            if (!log.params()) {
                return;
            }
        }
        Object[] arguments = methodInvocation.getArguments();
        DefaultParameterNameDiscoverer discover = new DefaultParameterNameDiscoverer();
        String[] parameters = discover.getParameterNames(methodInvocation.getMethod());
        if (parameters != null && arguments != null && arguments.length > 0) {
            JSONObject jsonObject = new JSONObject();
            for (int i = 0; i < parameters.length; i++) {
                Object argument = arguments[i];
                if (canJson(argument)) {
                    jsonObject.put(parameters[i], argument);
                }
            }
            doFilter(paramsFilters, jsonObject);
            logInfo.setParams(jsonObject.toString());
        }
    }

    /**
     * 返回值赋值
     *
     * @param log     /
     * @param logInfo /
     * @param proceed /
     */
    private void setupReturnValue(Log log, LogInfo logInfo, Object proceed) {
        if (proceed == null) {
            return;
        }
        Class<? extends JsonFilter>[] returnValueFilters = null;
        if (log == null) {
            if (!properties.getGlobalConfig().isReturnValue()) {
                return;
            }
        } else {
            returnValueFilters = log.returnValueFilters();
            if (!log.returnValue()) {
                return;
            }
        }
        String returnValue = null;
        try {
            if (canJson(proceed)) {
                JSONObject jsonObject = (JSONObject) JSONObject.toJSON(proceed);
                doFilter(returnValueFilters, jsonObject);
                returnValue = jsonObject.toJSONString();
            }
        } catch (Exception e) {
            returnValue = proceed.toString();
        }
        logInfo.setReturnValue(returnValue);
    }

    private void doFilter(Class<? extends JsonFilter>[] filters, JSONObject jsonObject) {
        if (filters == null || filters.length == 0) {
            return;
        }
        for (Class<? extends JsonFilter> jsonFilterClass : filters) {
            JsonFilter jsonFilter = jsonFilterCache.get(jsonFilterClass);
            if (jsonFilter == null) {
                continue;
            }
            jsonFilter.doFilter(jsonObject);
        }
    }

    private boolean canJson(Object object) {
        if (object == null) {
            return false;
        }
        return TypeToken.of(object.getClass()).getTypes().stream()
                .map(TypeToken::toString)
                .noneMatch(o -> DEFAULT_EXCLUDE_CLASSES_TO_JSON.contains(o)
                        || properties.getExcludeClassesToJson().contains(o));
    }

    @Override
    public void setApplicationContext(@NonNull ApplicationContext applicationContext) throws BeansException {
        jsonFilterCache = applicationContext.getBeansOfType(JsonFilter.class)
                .values()
                .stream()
                .collect(Collectors.toMap(Object::getClass, Function.identity()));
    }
}